using System;
using System.IO;
using System.Collections.Generic;

namespace Predictor
{	
    /// <summary>
    /// The class that gives the predictions
    /// Author: Panos
    /// </summary>

    public class Predictor
    {
		public static string wordnetPath = "./wordnet.txt";
		public static string cleanWordnetPath = "./clean_wordnet.txt";
		
        #region PrivateDataMembers

        private Trie root;
        private Dictionary<string, Statistics> knowledge;

        /* Current word typed */

        private string word;

        private int wordsTyped;

        /*Subtrie for the current word typed */

        private Trie subTrie;

        private char[] latinLetters = {
										'a','b', 'c', 'd', 'e', 'f', 'g',
		               					'h','i', 'j', 'k', 'l', 'm', 'n',
		               					'o', 'p', 'q', 'r', 's', 't', 'u',
		               					'v', 'w', 'x', 'y', 'z'
									  };

        private int personalizationFactor = 1;

        private bool unknownWord;

        #endregion


        #region PublicMethods

		/// <summary>
        /// Returns a dictionary containing the probability of each possible next character
        /// Author: Panos
		/// </summary>
		/// <returns>
		/// A <see cref="Dictionary<System.Char, System.Single>"/> containing pairs of possible next
		/// letter and it's probability to be typed
		/// </returns>

		public Predictor()
        {
            root = new Trie();
            root.LoadWordsFromFile(cleanWordnetPath);
            knowledge = new Dictionary<string, Statistics>();

            wordsTyped = 0;

            word = "";
            subTrie = root;
            unknownWord = false;
        }

        public Dictionary<char, float> GetPredictions()
        {
            int popularity;
            int postfixesCounter;
            float evaluation;

            float evaluationSum = 0;

            /* If subTrie is null, then the word being typed is not in the dictionary */

            if (subTrie == null)
            {
                unknownWord = true;
                return new Dictionary<char, float>();
            }

            /* Compute total evaluation amount */

            foreach (char possibleNextLetter in latinLetters)
            {
                popularity = subTrie.GetPopularity(possibleNextLetter);
                postfixesCounter = subTrie.GetSubTrieSize(possibleNextLetter);

                evaluation = Evaluate(popularity, postfixesCounter);

                evaluationSum += evaluation;
            }

            Dictionary<char, float> predictions = new Dictionary<char, float>();

            /* If there is no prediction */

            if (evaluationSum == 0)
            {
                foreach (char letter in latinLetters)
                {
                    predictions.Add(letter, 0);
                }

                return predictions;
            }
            else
            {
                foreach (char possibleNextLetter in latinLetters)
                {
                    popularity = subTrie.GetPopularity(possibleNextLetter);
                    postfixesCounter = subTrie.GetSubTrieSize(possibleNextLetter);

                    evaluation = Evaluate(popularity, postfixesCounter);

                    /* Normalize evaluation in order to express probability */

                    predictions.Add(possibleNextLetter, evaluation / evaluationSum);
                }
            }
            return predictions;
        }


        /// <summary>
		/// Informs the predictor that the parameter letter is typed.
		/// In order to give right predictions this method must be called whenever 
		/// there is a new input, even if this input is space
        /// Author: Panos
		/// </summary>
		/// <param name="word">
		/// A <see cref="System.String"/> indicating the word that was typed
		/// </param>

		public void CharacterTyped(char character)
        {
            if (character == ' ' || character == '.' || character == ',' || character == '!')
            {
                WordTyped();
            }
            else
            {
                word += character;

                /* If subTrie is null, then the word being typed is not in the dictionary */

                if (subTrie != null)
                {
                    subTrie = subTrie.GetSubTrie(character);
                }
                else
                {
                    /* Unknown word detected! */

                    unknownWord = true;
                }
            }
        }

		/// <summary>
		/// If the user cancels the prediction, this method MUST be called in order to inform the 
		/// prediction infrastructure
		/// Author: Panos
		/// </summary>

        public void PredictionCanceled()
        {
            word = "";
            subTrie = root;
            unknownWord = false;
        }


		/// <summary>
		/// Stores the instance of the predictor in a text file format in order to be loaded
		/// with the call of Load method
        /// Author: Panos
		/// </summary>
		/// <param name="filename">
		/// A <see cref="System.String"/> indicating the name of the file the predictor will be saved
		/// </param>

        public void Save(string filename)
        {
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }

            StreamWriter output = new StreamWriter(File.OpenWrite(filename));

            foreach (KeyValuePair<string, Statistics> data in knowledge)
            {
                output.WriteLine("{0}±{1}", data.Key, data.Value);
            }

            output.Close();
        }

		/// <summary>
		/// Creates and trains an instance of the predictor indicating the data in the stored file.
		/// The file must be generated in the past from the Save method in order to be in the right format
        /// Author: Panos
		/// </summary>
		/// <param name="filename">
		/// A <see cref="System.String"/> indicating the location of the serialized predictor instance
		/// </param>

        public void Load(string filename)
        {
            if (File.Exists(filename) == false)
            {
                return;
            }

            StreamReader input = File.OpenText(filename);

            string line;
            while ((line = input.ReadLine()) != null)
            {
                StringReader stringReader = new StringReader(line);

                int keyLenght = line.IndexOf('±');
                char[] keyArray = new char[keyLenght];

                stringReader.ReadBlock(keyArray, 0, keyLenght);
                string key = new string(keyArray);

                /*Skip the trail character '±' */

                stringReader.Read();

                Statistics statistics = new Statistics();
                statistics.LoadFromString(stringReader.ReadToEnd());

                knowledge.Add(key, statistics);
            }

            input.Close();

            GetTrained();
        }

        #endregion

        #region PrivateMethods

        /// <summary>
        /// Trains the Trie with the knowledge in order to give back personalized data
        /// Author: Panos
        /// </summary>

        private void GetTrained()
        {
            foreach (KeyValuePair<string, Statistics> data in knowledge)
            {
                root.WasTyped(data.Key, data.Value.GetPopularity());
                wordsTyped += data.Value.GetPopularity();
            }
        }


        /// <summary>
        /// This method is called by the CharacterTyped method when the typed character is space (' '), 
        /// in order to store the knowledge drained from this action
        /// Author: Panos
        /// </summary>

        private void WordTyped()
        {
            if (knowledge.ContainsKey(word) == false)
            {
                Statistics statistics = new Statistics();
                statistics.WordTyped();
                knowledge.Add(word, statistics);
            }
            else
            {
                knowledge[word].WordTyped();
            }

            /* If the word that was typed is known */

            if (unknownWord == false)
            {
                /* Train the Trie with the new knowledge */

                root.WasTyped(word, 1);
            }
            else
            {
                /* If the word is unknown, add it to the Trie */

                root.Add(word);

                /* Add word to wordnet */

                AddNewWordToWordNet();
            }

            wordsTyped++;

            word = "";
            subTrie = root;
            unknownWord = false;
        }


        /// <summary>
        /// Given the popularity of a possible next letter and the number of the postfixes that
        /// start with this letter, returns an evaluation
        /// Author: Panos
        /// </summary>
        /// <param name="popularity">
        /// A <see cref="System.Int32"/> indicating the popularity of the possible next letter
        /// after the current input
        /// </param>
        /// <param name="prefixesCounter">
        /// A <see cref="System.Int32"/>
        /// </param>
        /// <returns>
        /// A <see cref="System.Single"/> indicating the prediction evaluation for the letter
        /// </returns>

        private float Evaluate(int popularity, int prefixesCounter)
        {
            float usageRatio = personalizationFactor * wordsTyped / root.Size();

            /* If there are more typed words than the stored words (usageRatio > 1 ), 
             * the evaluation is computed based only on letter's popularity
             */

            if (usageRatio > 1)
            {
                usageRatio = 1;
            }

            return usageRatio * popularity + (1 - usageRatio) * prefixesCounter;
        }

        private void AddNewWordToWordNet()
        {
            StreamWriter streamWriter = File.AppendText(cleanWordnetPath);
            streamWriter.WriteLine(word);
            streamWriter.Close();
        }

        #endregion
    }
}

